package ss.pku.jsa.type.inference;

import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;

import org.eclipse.jdt.core.dom.ITypeBinding;
import org.eclipse.jdt.core.dom.IVariableBinding;

import ss.pku.jsa.type.constraint.solver.QError;


@SuppressWarnings({ "rawtypes", "unchecked" })
public class ObjectQType extends QType {
//	private static final int kind = 2;
	static final String OBJECT_TYPE_STRING = "Ljava/lang/Object;";
	static final TypeKey OBJECT_TYPE_KEY = TypeKey.getTypeKey(
			"Ljava/lang/Object;", 0, "java.lang.Object");

	private Set<Constraint> constraints = new HashSet();
	private HashMap<FieldKey, RefQType> fieldQTypes;
	private Map<TypeKey, ObjectQType> parentMap;
	public boolean isGlobal = false;

	private boolean isPreferred = true;

	boolean toplevelTested = false;
	public RefQType gtkPointerField;

	ObjectQType(TypeKey type, Location l, String name, boolean original) {
		this.name = name;
		this.l = l;
		this.q = Qualifier.makeQualifierVariable(this.name, l, original);
		this.type = type;

		this.parentMap = new HashMap();

		if (!(this instanceof ClassQType)) {
			this.parentMap.put(type, this);
		}

		this.fieldQTypes = new HashMap();
		JqualPlugin.analysis.objectQTypes.add(this);
	}

	public ObjectQType(ITypeBinding t, Location l, String name, boolean original) {
		this(t == null ? null : TypeKey.getTypeKey(t), l, name, original);

		if ((t != null) && (!t.isClass()) && (!t.isInterface()))
			throw new IllegalArgumentException(
					"Non-Simple type passed to constructor for ObjectQType. Argument was: "
							+ t);
	}

	public QType copy(Location l, String name) {
		Profile.logMemStart("ObjectQType/copy/new ObjectQType");
		ObjectQType ret = new ObjectQType(this.type, l, name, false);
		Profile.logMemEnd();
		return ret;
	}

	public void addFieldQType(IVariableBinding b, RefQType qt) {
		Profile.logMemStart("addFieldQType/new FieldKey");
		FieldKey key = FieldKey.getFieldKey(b);
		Profile.logMemEnd();
		qt.setKey(key);
		addFieldQType(key, qt);
	}

	public void addFieldQType(FieldKey key, RefQType qt) {
		if (qt == null) {
			throw new IllegalArgumentException(
					"Passed null qtype to addFieldQType");
		}

		if (key == null) {
			throw new IllegalArgumentException(
					"Passed null key to addFieldQType for qt: " + qt);
		}

		if (!(key instanceof ArrayFieldKey)) {
			JqualPlugin.analysis.fieldsUsed.add(key);
		}

		if ((!key.name.contains("pointer")) || (qt.getKey() == null)) {
			throw new IllegalArgumentException("never set key " + key + " for "
					+ qt);
		}
		if (this != this.ecr) {
			getObjEcr().addFieldQType(key, qt);
		}
		if (JqualPlugin.showContraintMsgs) {
			JqualPlugin.out.println("ADD Field: " + key + " to " + this);
		}
		if (this.fieldQTypes.containsKey(key)) {
			if (JqualPlugin.showContraintMsgs) {
				JqualPlugin.out.println("NO ADD: " + this + " already has "
						+ key);
			}

			Profile.logMemStart("addFieldQType/new Location");
			Location loc = new Location("Merging qtypes added as same field",
					0L, 0L);
			Profile.logMemEnd();
			Profile.logMemStart("addFieldQType/new MergeJob");
			MergeJob.addMergeJob(qt, (QType) this.fieldQTypes.get(key), loc,
					QError.MERGE);
			Profile.logMemEnd();
			return;
		}

		ClassQType cqt = null;
		if ((this instanceof ClassQType)) {
			cqt = (ClassQType) this;
		} else if ((key instanceof ArrayFieldKey)) {
			cqt = JqualPlugin.analysis.getClassQType(this.type);
		} else
			cqt = JqualPlugin.analysis.getClassQType(key.getDeclaringType());

		boolean useFieldSensitive = key.treatFieldSensitively();

		if (((qt.isStatic) || (!useFieldSensitive))
				&& (!(this instanceof ClassQType))) {
			cqt.addFieldQType(key, qt);
			return;
		}

		if (JqualPlugin.useJavariSelfLoopsHack) {
			qt.getQual().makeJavariHackShadowLT(this.l);
		}

		if (JqualPlugin.inferOpaque) {
//			if (key == null) {
//				throw new NullPointerException("Huh?");
//			}
			if (key.name == null) {
				throw new NullPointerException(key.toString());
			}
			if ((JqualPlugin.inferOpaque) && (key.name.contains("pointer"))
					&& (this.type.name.toLowerCase().contains("handle"))) {
				this.gtkPointerField = qt;
			}

		}

		if ((qt.isStatic) || (!useFieldSensitive) || (this.isGlobal)) {
			qt.setGlobal(true);
		}

		qt.setPreferred(this.isPreferred);
		String fs;
		if ((this instanceof ClassQType))
			fs = "<CLASS FIELD>";
		else {
			fs = useFieldSensitive ? "<FIELD-SENS>" : "<SHARED FIELD>";
		}

		qt.setName(this.name + "." + key.toString() + fs);

		if ((qt.deref() instanceof ObjectQType)) {
			ObjectQType contents = (ObjectQType) qt.deref();

			contents.getParentMap().putAll(getParentMap());

			TypeKey newType = contents.getType();

			if (getParentMap().containsKey(newType)) {
				qt.isRecursive = true;

				ObjectQType parent = (ObjectQType) this.parentMap.get(newType);
				if ((parent instanceof ClassQType)) {
					JqualPlugin.out.println(getParentMap());
					JqualPlugin.out.println("PARENT: " + parent);
					JqualPlugin.out.println(this);
					throw new RuntimeException(
							"Making recursive with ClassQType");
				}

				for (Map.Entry e : contents.getFieldQTypes().entrySet()) {
					FieldKey k = (FieldKey) e.getKey();
					RefQType f = (RefQType) e.getValue();
					parent.addFieldQType(k, f);
				}
				contents.getObjEcr().fieldQTypes = parent.getObjEcr().fieldQTypes;
			}

		}

		this.fieldQTypes.put(key, qt);

		if (useFieldSensitive) {
			Profile.logMemStart("addFieldQType/new FieldPropagationJob");
			FieldPropogationJob fpj = new FieldPropogationJob(this, qt);
			Profile.logMemEnd();
			JqualPlugin.analysis.constraintWorklist.push(fpj);
		}
	}

//	private String baseName(String name) {
//		int dotpos = name.lastIndexOf('.');
//		if (dotpos != -1) {
//			name = name.substring(dotpos + 1, name.length());
//		}
//
//		if (name.charAt(name.length() - 1) == '}') {
//			int pos = name.lastIndexOf('{');
//			name = name.substring(0, pos);
//		}
//		return name;
//	}

	public RefQType lookupFieldQType(IVariableBinding b, boolean isStatic) {
		Profile.logMemStart("lookupFieldQType/IVariableBinding/new FieldKey");
		FieldKey key = FieldKey.getFieldKey(b);
		Profile.logMemEnd();
		return lookupFieldQType(key, isStatic);
	}

	public RefQType lookupFieldQType(FieldKey key, boolean isStatic) {
		RefQType ret = null;
		if ((isStatic) || (!key.treatFieldSensitively())) {
			ClassQType cqt = null;

			if ((this instanceof ClassQType)) {
				cqt = (ClassQType) this;
			} else if ((key instanceof ArrayFieldKey)) {
				cqt = JqualPlugin.analysis.getClassQType(this.type);
			} else
				cqt = JqualPlugin.analysis
						.getClassQType(key.getDeclaringType());

			key.treatFieldSensitively();
			return cqt.lookupFieldQType(key);
		}

		ret = (RefQType) getFieldQTypes().get(key);

		return ret;
	}

	public RefQType lookupFieldQType(IVariableBinding f) {
		if (f == null)
			throw new NullPointerException();
		Profile.logMemStart("lookupFieldQType/IVariableBinding/new FieldKey");
		FieldKey fk = FieldKey.getFieldKey(f);
		Profile.logMemEnd();
		return lookupFieldQType(fk, (f.getModifiers() & 0x8) != 0);
	}

	public void makeInstantiationEdges() {
		ClassQType cqt = JqualPlugin.analysis.getClassQType(this.type);

		if (JqualPlugin.contextSensitive)
			Constraint.addConstraint(cqt, this, QualGate.EMPTY, this.l, 1,
					QError.CLASS, Constraint.Kind.LT);
		else
			Constraint.addConstraint(cqt, this, QualGate.EMPTY, this.l, 0,
					QError.CLASS, Constraint.Kind.LT);
	}

	public boolean isPrimitive() {
		return false;
	}

	public int getKind() {
		return 2;
	}

	public void setOriginal(boolean original) {
		this.original = original;
	}

	public void setGlobal(boolean global) {
		if (this.isGlobal == global) {
			return;
		}
		this.isGlobal = global;
		this.q.setGlobal(global);

		for (RefQType f : this.fieldQTypes.values())
			f.setGlobal(global);
	}

	public void setPreferred(boolean preferred) {
		if (this.isPreferred == preferred) {
			return;
		}
		this.isPreferred = preferred;
		this.q.setPreferred(preferred);

		for (RefQType f : this.fieldQTypes.values())
			f.setPreferred(preferred);
	}

	public boolean isArray() {
		return false;
	}

	public String toString() {
		return this.name;
	}

	public int size() {
		return this.fieldQTypes.size();
	}

	public String dump() {
		StringBuffer ret = new StringBuffer();
		ObjectQType thisEcr = getObjEcr();

		if (thisEcr != this) {
			ret.append("<<<");
		}
		if ((this instanceof ClassQType))
			ret.append("ClassQType ");
		else {
			ret.append("ObjectQType ");
		}
		ret.append(this.name + " (" + this.type + ")");

		if (this.isGlobal) {
			ret.append("(global)");
		}
		if (thisEcr != this) {
			ret.append(" ECR: " + thisEcr.name + ">>>");
		}
		if ((!JqualPlugin.fieldSensitive)
				&& (JqualPlugin.obeyFSAnnotations)
				&& (JqualPlugin.analysis.fieldSensitiveTypes
						.contains(this.type))) {
			ret.append(" - FIELD SENSITIVE");
		}
		ret.append("\n");

		Set actualConstraints = getConstraints();
		Map actualFieldQTypes = getFieldQTypes();
		Map actualParentMap = getParentMap();

		if (!actualConstraints.isEmpty()) {
			ret.append("constraints:");
			ret.append("\n");
			for (Iterator i = actualConstraints.iterator(); i.hasNext();) {
				ret.append("   " + i.next());
				ret.append("\n");
			}
		}

		if (!actualFieldQTypes.isEmpty()) {
			ret.append("fields:");
			ret.append("\n");
			for (Iterator i = actualFieldQTypes.entrySet().iterator(); i
					.hasNext();) {
				Map.Entry e = (Map.Entry) (Map.Entry) i.next();
				RefQType f = (RefQType) e.getValue();
				String fname = f.getName();

				int dotpos = fname.lastIndexOf('.');
				if (dotpos != -1) {
					fname = fname.substring(dotpos + 1, fname.length());
				}
				ret.append("   " + fname + " (" + f.deref().getName() + ")");
				ret.append(" (fieldKey: " + f.getKey() + ")");
				if (f.isGlobal)
					ret.append("(global)");
				ret.append("\n");
			}
		}
		if (!actualParentMap.isEmpty()) {
			ret.append("parent types: ");
			for (Iterator i = actualParentMap.keySet().iterator(); i.hasNext();) {
				ret.append(i.next() + " ");
			}
			ret.append("\n");
		}
		ret.append("size: " + getSize());
		ret.append("\n");
		return ret.toString();
	}

	public ObjectQType getObjEcr() {
		return (ObjectQType) getEcr();
	}

	public Set<Constraint> getConstraints() {
		return getObjEcr().constraints;
	}

	public Map<TypeKey, ObjectQType> getParentMap() {
		return getObjEcr().parentMap;
	}

	public Map<FieldKey, RefQType> getFieldQTypes() {
		return getObjEcr().fieldQTypes;
	}

	public void merge(QType qt, Location loc, QError error) {
		if (!(qt instanceof ObjectQType)) {
			throw new IllegalArgumentException("Merging non-object QType " + qt
					+ " with " + this);
		}
		if (((qt instanceof ClassQType)) || ((this instanceof ClassQType))) {
			JqualPlugin.out.println("Merging " + this + " with " + qt);
		}
		ObjectQType newQType = ((ObjectQType) qt).getObjEcr();

		if (equals(newQType)) {
			return;
		}
		CQual.makeEdge(Constraint.Kind.EQ, getQual(), newQType.getQual(),
				QualGate.EMPTY, loc, 0, error);

		ObjectQType oldEcr = getObjEcr();
		this.ecr = newQType;

		Map newFields = newQType.getFieldQTypes();

		for (Map.Entry e : oldEcr.fieldQTypes.entrySet()) {
			FieldKey key = (FieldKey) e.getKey();
			RefQType fqt = (RefQType) e.getValue();

			if (newFields.containsKey(key)) {
				if (((RefQType) newFields.get(key)).equals(fqt)) {
					continue;
				}
				Profile.logMemStart("ObjectQType/merge/new MergeJob");
				MergeJob.addMergeJob(fqt, (QType) newFields.get(key), loc,
						error);
				Profile.logMemEnd();
			} else {
				newQType.addFieldQType(key, fqt);
			}

		}

		for (Constraint c : getConstraints())
			if (equals(c.to)) {
				c.moveTarget(newQType);
			} else {
				c.moveSource(newQType);
			}
	}

	public int getSize() {
		int ret = 1;
		getObjEcr();

		Map<FieldKey, RefQType> actualFieldQTypes = getFieldQTypes();

		for (QType q : actualFieldQTypes.values()) {
			ret += q.getSize();
		}
		return ret;
	}

	public String toStringWithQuals(int fieldIndent) {
		StringBuffer ret = new StringBuffer();
		String quals = getEcr().q.getInferred();
		ret.append(quals + "[");

		if (!getFieldQTypes().isEmpty()) {
			ret.append("\n");
		}

		Iterator localIterator = new TreeMap(getFieldQTypes()).entrySet()
				.iterator();

		while (localIterator.hasNext()) {
			Map.Entry entry = (Map.Entry) localIterator.next();
			RefQType f = (RefQType) entry.getValue();
			FieldKey key = (FieldKey) entry.getKey();

			for (int i = 0; i < fieldIndent + 1; i++)
				ret.append("   ");
			ret.append(key.getShortName() + ":");
			ret.append(f.toStringWithQuals(fieldIndent + 1));
			ret.append("\n");
		}
		if (!getFieldQTypes().isEmpty()) {
			for (int i = 0; i < fieldIndent; i++) {
				ret.append("   ");
			}
		}
		ret.append("]");

		return ret.toString();
	}
}
